S3 オブジェクトのContentTypeを作成イベント通知(Lambda)で更新してみた
AWS事業本部 梶原@福岡オフィスです。
S3に保存、アップロードしたタイミングで、ContentTypeを更新したいというシチュエーションがありましたので、作成してみました。
こちらのブログの派生版になります
ユースケースとしては、ContenTypeにcharsetを追加付与したい、S3で自動で設定されるContentTypeを変更したいなどというシチュエーションなどで、役に立つかと思います。
注意
オブジェクト作成が大量に頻繁にあるようなバケットの場合はLambdaの実行回数などの上限、また、Lambdaの実行、S3APIの呼び出しは従量課金となりますので、意図しない動きなどにご注意しつつご検証の上ご使用ください
メタデータの更新(ContentTypeの更新はオブジェクトの複製(再作成)となるため、イベントの作成ループなどには重々お気をつけください。
また、バケットのバージョニングが有効な場合などは、対象のオブジェクトのバージョンが1つあがるため、バージョニングが有効なバケットについては、対象のオブジェクトに対しては倍の容量がかかることもご承知おきください。
説明
既存のバケットにS3イベント通知を作成する方法はこちらのブログで説明しているの割愛します。
CloudFormation一撃で作成できるようにしていますので、ブログの最後のテンプレートで対象のバケットを指定してスタックを作成してください。
パラメータ
Parameters: NotificationBucket: Type: String Description: S3 bucket Prefix: Type: String Description: Prefix of the object key name for filtering rules. Suffix: Type: String Default: .txt Description: Suffix of the object key name for filtering rules
NotificationBucket: オブジェクト作成時にLambda関数を呼び出す既存のバケットになります
Prefix: 対象のバケットのディレクトリを指定してください
Suffix: 拡張子になります。お好みの拡張子を指定してください
S3オブジェクトのContentTypeの更新関数
ContentTypeUpdateFunction: Type: 'AWS::Lambda::Function' Properties: Code: ZipFile: | import json import urllib.parse import posixpath import boto3 print('Loading function') s3 = boto3.client('s3') def lambda_handler(event, context): # print("Received event: " + json.dumps(event, indent=2)) # Get the object from the event and show its content type bucket = event['Records'][0]['s3']['bucket']['name'] key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8') try: response = s3.get_object( Bucket = bucket, Key = key ) print(response) contentType = response['ContentType'] print(contentType) if 'charset' in contentType: print('Already ContentType charset' + contentType) else: contentType += '; charset=UTF-8' print(contentType) response = s3.copy_object( Bucket = bucket, Key = key, CopySource = {'Bucket': bucket, 'Key': key}, ContentType = contentType, MetadataDirective="REPLACE" ) except Exception as e: print(e) print('Error copy object {} from bucket {}. Make sure they exist and your bucket is in the same region as this function.'.format(key, bucket)) raise e Handler: index.lambda_handler Role: !GetAtt ContentTypeUpdateFunctionRole.Arn Runtime: python3.8 Timeout: 60
既存のContentTypeに対して、'; charset=UTF-8'を付与しています。
上書き、再呼び出しを防ぐため、charsetが含まれている場合は処理を実施していません
- オブジェクトの既存のContentTypeを取得する
- 既存ContentTypeに、charset=UTF-8を追加する
- ContentTypeを指定しつつオブジェクトをコピーして、再作成する
設定するContentTypeなど、拡張子や、一時的にオブジェクトを読み込むなどカスタマイズすれば、独自の拡張も行えるかと思いますので、ご自由に変更してください
S3オブジェクトのContentTypeの更新関数のRole
ContentTypeUpdateFunctionRole: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - 'sts:AssumeRole' Path: / Policies: - PolicyName: root PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 's3:ListBucket' Resource: !Sub 'arn:aws:s3:::${NotificationBucket}' - Effect: Allow Action: - 's3:GetObject' - 's3:PutObject' - 's3:CopyObject' Resource: !Sub 'arn:aws:s3:::${NotificationBucket}/*' - Effect: Allow Action: - 'logs:CreateLogGroup' - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: 'arn:aws:logs:*:*:*'
S3バケットオブジェクトのCopyObjectを行うため必要な
s3:ListBucket, s3:GetObject, s3:PutObject, s3:CopyObject
を付与しています。
Copyイベントループを避けています。
再作成時に、Copyを行っているため、設定するS3に設定する作成イベントは's3:ObjectCreated:*' ではなく、 Copy以外のイベントを設定しています。
['s3:ObjectCreated:Put', 's3:ObjectCreated:Post', 's3:ObjectCreated:CompleteMultipartUpload']
動作検証
スタックが正常に作成されたら。指定したバケットにファイルをアップロードしてみてください。
動作検証としてmemo.txtファイルを保存してみます。
保存タイミングでLambda関数が呼び出され自動でContentTypeが更新されます
まとめ
S3 sync などで、まとめて同期している場合は、コンテンツタイプは自動生成に依存してしまうかと思いますので
オブジェクト作成のタイミングでContentTypeを指定したい、変更したいといった場合に役に立つと嬉しいです。
Template
AWSTemplateFormatVersion: 2010-09-09 Description: >- Update S3 Object ContentType use S3 CreateTrigger. Parameters: NotificationBucket: Type: String Description: S3 bucket Prefix: Type: String Description: Prefix of the object key name for filtering rules. Suffix: Type: String Default: .txt Description: Suffix of the object key name for filtering rules Resources: ContentTypeUpdateFunction: Type: 'AWS::Lambda::Function' Properties: Code: ZipFile: | import json import urllib.parse import posixpath import boto3 print('Loading function') s3 = boto3.client('s3') def lambda_handler(event, context): # print("Received event: " + json.dumps(event, indent=2)) # Get the object from the event and show its content type bucket = event['Records'][0]['s3']['bucket']['name'] key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8') try: response = s3.get_object( Bucket = bucket, Key = key ) print(response) contentType = response['ContentType'] print(contentType) if 'charset' in contentType: print('Already ContentType charset' + contentType) else: contentType += '; charset=UTF-8' print(contentType) response = s3.copy_object( Bucket = bucket, Key = key, CopySource = {'Bucket': bucket, 'Key': key}, ContentType = contentType, MetadataDirective="REPLACE" ) except Exception as e: print(e) print('Error copy object {} from bucket {}. Make sure they exist and your bucket is in the same region as this function.'.format(key, bucket)) raise e Handler: index.lambda_handler Role: !GetAtt ContentTypeUpdateFunctionRole.Arn Runtime: python3.8 Timeout: 60 ContentTypeUpdateFunctionRole: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - 'sts:AssumeRole' Path: / Policies: - PolicyName: root PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 's3:ListBucket' Resource: !Sub 'arn:aws:s3:::${NotificationBucket}' - Effect: Allow Action: - 's3:GetObject' - 's3:PutObject' - 's3:CopyObject' Resource: !Sub 'arn:aws:s3:::${NotificationBucket}/*' - Effect: Allow Action: - 'logs:CreateLogGroup' - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: 'arn:aws:logs:*:*:*' LambdaInvokePermission: Type: 'AWS::Lambda::Permission' Properties: FunctionName: !GetAtt ContentTypeUpdateFunction.Arn Action: 'lambda:InvokeFunction' Principal: s3.amazonaws.com SourceAccount: !Ref 'AWS::AccountId' SourceArn: !Sub 'arn:aws:s3:::${NotificationBucket}' LambdaTrigger: Type: 'Custom::LambdaTrigger' DependsOn: LambdaInvokePermission Properties: ServiceToken: !GetAtt PutBucketNotificationFunction.Arn Id: !Sub - S3LambdaNotif-${UniqueId} - UniqueId: !Select [0, !Split ['-', !Select [2, !Split [/, !Ref 'AWS::StackId']]]] Bucket: !Ref NotificationBucket Prefix: !Ref Prefix Suffix: !Ref Suffix LambdaArn: !GetAtt ContentTypeUpdateFunction.Arn PutBucketNotificationFunction: Type: 'AWS::Lambda::Function' Properties: Handler: index.lambda_handler Role: !GetAtt PutBucketNotificationFunctionRole.Arn Code: ZipFile: | import json import boto3 import cfnresponse SUCCESS = "SUCCESS" FAILED = "FAILED" print('Loading function') s3 = boto3.resource('s3') def lambda_handler(event, context): print("Received event: " + json.dumps(event, indent=2)) responseData={} try: if event['RequestType'] == 'Delete': print("Request Type:",event['RequestType']) Id=event['ResourceProperties']['Id'] Bucket=event['ResourceProperties']['Bucket'] delete_notification(Id, Bucket) print("Sending response to custom resource after Delete") elif event['RequestType'] == 'Create' or event['RequestType'] == 'Update': print("Request Type:",event['RequestType']) Id=event['ResourceProperties']['Id'] Prefix=event['ResourceProperties']['Prefix'] Suffix=event['ResourceProperties']['Suffix'] LambdaArn=event['ResourceProperties']['LambdaArn'] Bucket=event['ResourceProperties']['Bucket'] add_notification(Id, Prefix, Suffix, LambdaArn, Bucket) responseData={'Bucket':Bucket} print("Sending response to custom resource") responseStatus = 'SUCCESS' except Exception as e: print('Failed to process:', e) responseStatus = 'FAILED' responseData = {'Failure': 'Something bad happened.'} cfnresponse.send(event, context, responseStatus, responseData) def add_notification(Id, Prefix, Suffix, LambdaArn, Bucket): bucket_notification = s3.BucketNotification(Bucket) print(bucket_notification.lambda_function_configurations) lambda_function_configurations = bucket_notification.lambda_function_configurations if lambda_function_configurations is None: lambda_function_configurations = [] else: lambda_function_configurations = [e for e in lambda_function_configurations if e['Id'] != Id] lambda_config = {} lambda_config['Id'] = Id lambda_config['LambdaFunctionArn'] = LambdaArn lambda_config['Events'] = ['s3:ObjectCreated:Put', 's3:ObjectCreated:Post', 's3:ObjectCreated:CompleteMultipartUpload'] lambda_config['Filter'] = {'Key': {'FilterRules': [ {'Name': 'Prefix', 'Value': Prefix}, {'Name': 'Suffix', 'Value': Suffix} ]} } lambda_function_configurations.append(lambda_config) print(lambda_function_configurations) put_bucket_notification(bucket_notification, lambda_function_configurations) print("Put request completed....") def delete_notification(Id, Bucket): bucket_notification = s3.BucketNotification(Bucket) print(bucket_notification.lambda_function_configurations) lambda_function_configurations = bucket_notification.lambda_function_configurations if lambda_function_configurations is not None: lambda_function_configurations = [e for e in lambda_function_configurations if e['Id'] != Id] print(lambda_function_configurations) put_bucket_notification(bucket_notification, lambda_function_configurations) print("Delete request completed....") def put_bucket_notification(BucketNotification, LambdaFunctionConfigurations): notification_configuration = {} if LambdaFunctionConfigurations is not None: notification_configuration['LambdaFunctionConfigurations'] = LambdaFunctionConfigurations if BucketNotification.queue_configurations is not None: notification_configuration['QueueConfigurations'] = BucketNotification.queue_configurations if BucketNotification.topic_configurations is not None: notification_configuration['TopicConfigurations'] = BucketNotification.topic_configurations print(notification_configuration) response = BucketNotification.put( NotificationConfiguration= notification_configuration ) Runtime: python3.8 Timeout: 50 PutBucketNotificationFunctionRole: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - 'sts:AssumeRole' Path: / Policies: - PolicyName: root PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 's3:GetBucketNotification' - 's3:PutBucketNotification' Resource: !Sub 'arn:aws:s3:::${NotificationBucket}' - Effect: Allow Action: - 'logs:CreateLogGroup' - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: 'arn:aws:logs:*:*:*'
参考情報
CloudFormation 一撃で既存のS3バケットでAWS LambdaのS3のオブジェクト作成通知を追加作成してみた